/*
 * Logic.java 
 * 
 * author: The MomberBan Team 
 *    date: 12.09.2008
 *    
 */

package at.momberban.game.me;

import java.io.IOException;
import java.util.Enumeration;

import javax.microedition.lcdui.Canvas;

/**
 * local in-game logic.
 */
public abstract class Logic {

    private static final int TILE_FREE = 20;

    private static final int TILE_BREAKABLE = 21;

    private static final int TILE_UNBREAKABLE = 22;

    private static final int TILE_OCCUPIED = 23;

    protected NetworkManager networkManager = null;

    protected Game game;

    protected Player me;

    protected long now;

    private SynchronizedQueue eventQueue = Event.getEventQueue();

    /**
     * round a double value
     * 
     * @param value
     * @return
     */
    protected static final int round(double value) {
        double tmp = Math.floor(value);

        if (value - tmp >= 0.5) {
            tmp += 1;
        }

        return (int) tmp;
    }

    /**
     * constructor
     * 
     * @param game
     */
    public Logic(Game game) {
        this.game = game;
        this.networkManager = game.getNetworkManager();

        if (this.networkManager == null) {
            throw new RuntimeException("no network manager");
        }
    }

    /**
     * decode events
     */
    protected void handleEvents() {
        while (!eventQueue.isEmpty()) {
            Event e = (Event) eventQueue.remove();
            try {
                handle(e, e.getID(), e.getData());
            } catch (NullPointerException nex) {
                Logger.log("Null Pointer in game handling");
                nex.printStackTrace();
                throw new RuntimeException(nex.getMessage());
            }
        }
    }

    /**
     * handle general events
     * 
     * @param e
     *            the decoded event
     * @param eventId
     *            the event id
     * @param data
     *            the event payload
     */
    public void handle(Event e, char eventId, String[] data) {
        switch (eventId) {
            case Event.MOVE_PLAYER: {
                Player p = getPlayerForString(data[0]);
                int x = Integer.parseInt(data[1]);
                int y = Integer.parseInt(data[2]);
                int direction = Integer.parseInt(data[3]);
                movePlayer(p, x, y, direction);
                break;
            }
            case Event.BOMB_PLANTED: {
                Player p = getPlayerForString(data[0]);
                if (p != this.me) {
                    int posId = Integer.parseInt(data[1]);
                    int range = Integer.parseInt(data[2]);
                    plantBomb(p, posId, range);
                }
                break;
            }
            case Event.LEAVE_EVENT: {
                Integer id = Integer.valueOf(data[0]);
                removePlayer(id);
                break;
            }
            case Event.JOIN_EVENT: {
                joinPlayer(Integer.parseInt(data[0]), data[1], Long
                        .parseLong(data[2]));
                break;
            }
            default:
                throw new RuntimeException("unknown event #" + e.getID()
                        + " : " + e.getEncoded());
        }
    }

    /**
     * retrieve the player from the game
     * 
     * @param string
     * @return
     */
    protected Player getPlayerForString(String key) {
        return (Player) this.game.getPlayers().get(IntegerCache.valueOf(key));
    }

    /**
     * remove a player from the game
     * 
     * @param id
     */
    protected void removePlayer(Integer id) {
        this.game.getPlayers().remove(id);
    }

    /**
     * add a joined player to the game
     * 
     * @param id
     * @param name
     * @param challenge
     */
    protected void joinPlayer(int id, String name, long challenge) {
//        if (this.game.getPlayers().get(IntegerCache.valueOf(id)) == null) {
            Logger.log("create new player: " + id);
            Player newPlayer = new Player(id);
            newPlayer.setName(name);
            this.game.getPlayers().put(IntegerCache.valueOf(id), newPlayer);
//        }
    }

    /**
     * move a player on the map. declared final, because player moving without
     * network activity is required (for the case, that the client receives a
     * opponent player move, without moving himself)
     * 
     * @param player
     * @param x
     * @param y
     * @param direction
     */
    protected final void movePlayer(Player player, int x, int y, int direction) {
        player.setPosition(x, y);
        player.setDirection(direction);
    }

    /**
     * client player moving: get player with coordinates. then call movePlayer
     * 
     * @param direction
     *            the direction to move
     */
    protected void moveKey(int direction) {
        if (me == null) {
            Logger.log("cannot move myself");
        } else {
            boolean dirChange = this.me.getDirection() != direction;
            if (dirChange) {
                Logger.log("direction change!");
            }

            int newX = this.me.getX();
            int newY = this.me.getY();

            switch (direction) {
                case Canvas.DOWN:
                    newY += this.me.getSpeed();
                    break;
                case Canvas.UP:
                    newY -= this.me.getSpeed();
                    break;
                case Canvas.LEFT:
                    newX -= this.me.getSpeed();
                    break;
                case Canvas.RIGHT:
                    newX += this.me.getSpeed();
                    break;
                default:
                    throw new RuntimeException("unknown move");
            }

            // if new position is within the range
            if (newX >= 0
                    && newX <= this.game.getMap().getWidth()
                            - Player.PLAYER_WIDTH
                    && newY >= 0
                    && newY <= this.game.getMap().getHeight()
                            - Player.PLAYER_HEIGHT) {
                int[] old = { this.me.getX(), this.me.getY() };
                movePlayer(this.me, newX, newY, direction);

                // collision check
                if (isCollision() && !this.me.isDead()) {
                    // smooth check
                    int[] pos = getTargetForPosition(old[0], old[1],
                            Player.PLAYER_WIDTH);

                    // TODO: fix a wallhack
                    if (direction == Canvas.UP || direction == Canvas.DOWN) {
                        // predict x
                        movePlayer(me, pos[1], newY, direction);
                        if (!isCollision()) {
                            old[0] = pos[1];
                        }
                    } else {
                        // predict y
                        movePlayer(this.me, newX, pos[2], direction);
                        if (!isCollision()) {
                            old[1] = pos[2];
                        }
                    }

                    // restore position
                    movePlayer(this.me, old[0], old[1], direction);
                }

            } else {
                Logger.log("i'm touching the voiiid: " + newX + ":" + newY);
            }
        }
    }

    /**
     * send the move across the network
     */
    protected final void sendMove(int direction) {
        // now send it
        int position = this.me.getX() + this.me.getY() + direction;

        if (this.me.getLastPosition() != position) {
            this.me.setLastPosition(position);

            // send the move to the server
            String[] data = { String.valueOf(this.me.getId()),
                    String.valueOf(this.me.getX()),
                    String.valueOf(this.me.getY()), String.valueOf(direction) };

            send(Event.MOVE_PLAYER, data);
        }
    }

    /**
     * send a new bomb across the network
     * 
     * @param player
     * @param bombId
     */
    protected final void sendBombPlant(Bomb bomb, Player player) {
        // send it if its created from myself
        if (bomb != null && player == this.me) {
            String data[] = { String.valueOf(player.getId()),
                    String.valueOf(bomb.getPositionId()),
                    String.valueOf(bomb.getExplosionRange()) };
            send(Event.BOMB_PLANTED, data);
        }
    }

    /**
     * check if i'm collided
     * 
     * @return
     */
    private boolean isCollision() {        
        int[] playerPos = getTargetForPosition(this.me.getX(), this.me.getY(), Player.PLAYER_WIDTH);

        int[] pos = null;
        int dx = 0;
        int dy = 0;

        boolean check = false;
        switch (this.me.getDirection()) {
            case Canvas.UP:
                if (playerPos[2] > this.me.getY()) {
                    dy = -this.game.getMap().getBackgroundLayer().getCellHeight();
                    if (playerPos[2] + dy > -game.getMap().getBackgroundLayer().getCellHeight() / 2) {
                        check = true;
                    }
                }
                break;
            case Canvas.RIGHT:
                int width = this.game.getMap().getBackgroundLayer().getCellWidth();
                if (playerPos[1] + width < this.me.getX() + Player.PLAYER_WIDTH) {
                    dx = width;
                    if (playerPos[1] + dx < this.game.getMap().getBackgroundLayer().getWidth()
                        + game.getMap().getBackgroundLayer().getCellHeight() / 2) {
                        
                        check = true;
                    }
                }
                break;
            case Canvas.DOWN:
                int height = this.game.getMap().getBackgroundLayer().getCellHeight();
                if (playerPos[2] + height < this.me.getY() + Player.PLAYER_HEIGHT) {
                    dy = height;
                    if (playerPos[2] + dy < this.game.getMap().getBackgroundLayer().getHeight()
                        + game.getMap().getBackgroundLayer().getCellHeight() / 2) {
                        
                        check = true;
                    }
                }
                break;
            case Canvas.LEFT:
                if (playerPos[1] > this.me.getX()) {
                    dx = -this.game.getMap().getBackgroundLayer().getCellWidth();
                    if (playerPos[1] + dx > -game.getMap().getBackgroundLayer().getCellWidth() / 2) {
                        check = true;
                    }
                }
                break;
        }

        if (check) {
            pos = getTargetForPosition(playerPos[1] + dx, playerPos[2] + dy, Player.PLAYER_WIDTH);
        } else {
            pos = new int[] {0, 0, 0, TILE_FREE};
        }
        
        return ((pos == null) ||(pos[3] == TILE_OCCUPIED)
            || (this.me.collidesWith(this.game.getMap().getBreakableLayer(), true))
            || (this.me.collidesWith(this.game.getMap().getUnbreakableLayer(), true)));
    }

    /**
     * handle actions
     * 
     * @param action
     *            the game action
     */
    protected void actionKey(int action) {
        if (! this.me.isDead()) {
            switch (action) {
                case Canvas.FIRE:
                case Canvas.GAME_A:
                    plantBomb(this.me, 0, this.me.getBombsRange());
                    break;
                case Canvas.GAME_B:
                    timeBomb(this.me);
                    // more stuff here too
                    break;
            }
        }
    }

    /**
     * let it explode
     * 
     * @param player
     */
    protected void timeBomb(Player player) {
        // explode!
        player.setBombs(player.getMaxBombs());
    }

    /**
     * plant a bomb on the screen
     * 
     * @param player
     *            the player who tried to plant the bomb
     * @param posId
     *            the bomb id
     * @return a bomb reference, if it's created in the game
     */
    protected Bomb plantBomb(Player player, int posId, int range) {
        Bomb bomb = null;
        int canPlant = player.getBombs();

        if (canPlant > 0) {
            int[] pos;

            // client created
            if (player == this.me) {
                pos = getTargetForPosition(player.getX(), player.getY(),
                        Bomb.BOMB_WIDTH);
            } else { // event created
                pos = getPositionForTarget(posId, Bomb.BOMB_WIDTH);
            }

            Logger.log("bomb plant: " + pos[0] + ":" + pos[1] + ":" + pos[2]);

            // if this place is free for planting a bomb
            if (pos[3] == TILE_FREE) {
                player.setBombs(canPlant - 1);
                bomb = new Bomb();
                bomb.setPositionId(pos[0]);
                bomb.setPosition(pos[1], pos[2]);
                bomb.setPlayer(player);
                bomb.setTimePlanted(System.currentTimeMillis());
                bomb.setExplosionRange(range);

                // server needs to take care of this generated id
                this.game.getBombs().put(IntegerCache.valueOf(pos[0]), bomb);
            } else {
                Logger.log("cannot plant. not free");
            }
        }

        return bomb;
    }

    /**
     * get the target tile coordinates on the map. check the tile, the player is
     * within and assign a id for that position [0]. the other two parts are the
     * new coordinates <code>
     * tile w = 20, 100 total line, what is the tile id then?
     *****************************
     *      20 20 20 20 20 * [100]
     *****************************
     * 20 * 0  1  2  3  4  *
     * 20 * 5  6  7  8  9 *
     * 20 * 10 11 12 13 14 *
     * 20 * 15 16 17 18 19 *
     * *********************
     * [80]
     * 
     * 20/100 = 1/5 = 0.2 ; * tX = 1 (id)
     * 100/100 = 1; * tx = 5;
     * 30/100 = 0.3; * tx = 1.5 -> ceil = 2 (id)
     * 90/100 = 0.9; * tx = 4,5 -> ceil = 5 (id)
     * 
     * 20/80 = 0.25 * ty = 1 
     * 0/80 = 0 * ty = 0
     * </code>
     * 
     * @param x
     *            current x
     * @param y
     *            current y
     * @param spriteWidth
     *            the sprite width
     * @return an array of three elements [id, x, y]
     */
    private final int[] getTargetForPosition(int x, int y, int spriteWidth) {
        GameMap map = this.game.getMap();

        int w = map.getWidth(); // map width
        int h = map.getHeight(); // map height
        int tX = map.getXTiles(); // amount of x tiles
        int tY = map.getYTiles(); // amount of y tiles

        // the x offset is used to compensate the sprite width with the tile
        // width
        int offsetX = ((w / tX) - spriteWidth) / 2;

        int colId = round((double) (x - offsetX) * tX / w);
        int rowId = round((double) y * tY / h);

        return getTileInfo(colId, rowId, offsetX);
    }

    /**
     * build some info for a tile
     * 
     * @param colId
     * @param rowId
     * @return
     */
    private final int[] getTileInfo(int colId, int rowId, int offsetX) {
        int ret[] = new int[4];
        GameMap map = this.game.getMap();

        int w = map.getWidth();
        int h = map.getHeight();
        int tX = map.getXTiles();
        int tY = map.getYTiles();

        ret[0] = colId + rowId * tX; // tile Id
        ret[1] = colId * w / tX + offsetX; // tile x - coordinate
        ret[2] = rowId * h / tY; // tile y - coordinate

        // build the tile status
        if (map.getBreakableLayer().getCell(colId, rowId) == 1) {
            Logger.log("plant: breakable");
            ret[3] = TILE_BREAKABLE;
        } else if (map.getUnbreakableLayer().getCell(colId, rowId) == 1) {
            Logger.log("plant: unbreakable");
            ret[3] = TILE_UNBREAKABLE;
        } else if (this.game.getBombs().get(IntegerCache.valueOf(ret[0])) != null) {
            Logger.log("plant: occupied");
            ret[3] = TILE_OCCUPIED;
        } else {
            ret[3] = TILE_FREE;
        }

        return ret;
    }

    /**
     * return the tile info for the target
     * 
     * @param pos
     *            (x,y) coordinate pair
     * @param spriteWidth
     *            the sprite width
     * @return
     */
    private final int[] getPositionForTarget(int pos, int spriteWidth) {
        GameMap map = this.game.getMap();

        int tX = map.getXTiles();
        int w = map.getWidth();

        int colId = pos % tX;
        int rowId = pos / tX;

        int offsetX = ((w / tX) - spriteWidth) / 2;

        return getTileInfo(colId, rowId, offsetX);
    }

    /**
     * let a bomb explode. can force additional bombs to explode. let player
     * bomb stock increase by one.
     * 
     * @param bombId
     * @param b
     */
    public void explode(Integer bombId, Bomb b) {
        Logger.log("explode: " + bombId);
        Player p = b.getPlayer();
        if (p != null) {
            p.setBombs(p.getBombs() + 1);
        }

        // TODO: check which bombs have to explode
        if (!b.isTriggered()) {
            b.setTriggered(true);
        }
    }

    /**
     * handle a game tick
     */
    public void gameTick() {
        this.now = System.currentTimeMillis();
    }

    /**
     * spawn a player on the map
     * 
     * @param player
     *            the player to spawn
     */
    public final void spawnPlayer(Player player) {
        Logger.log("player " + player.getId() + " spawned");
        int spawnX = this.game.getMap().getXSpawn(player.getId());
        int spawnY = this.game.getMap().getYSpawn(player.getId());

        player.setPosition(spawnX, spawnY);
    }

    /**
     * start the game
     */
    public void startGame() {
        // assign myself
        this.me = this.game.getPlayer();
        if (this.me == null) {
            throw new RuntimeException("don't know my player id");
        }
        Logger.log("######### GAME STARTED AS PLAYER " + this.me.getId()
                + " #########");

        // spawn all players
        Enumeration i = this.game.getPlayers().keys();
        while (i.hasMoreElements()) {
            Player p = (Player) this.game.getPlayers().get(i.nextElement());
            spawnPlayer(p);
        }
    }

    /**
     * a player died, recognized by the canvas sprite collision detection.
     * 
     * @param victim
     *            who died
     * @param killer
     *            which bomb did make the mess
     */
    public void playerDied(Player victim, Player killer) {
        if (!victim.isDead()) {
            victim.setDead(true);
            /*
             * update statistics
             */
            if (killer == victim) { // selfkill
                killer.setFrags(this.me.getFrags() - 1);
                killer.setDeaths(this.me.getDeaths() + 1);
                if (killer == this.me) {
                    Logger.log("damn shit, i killed myself");
                } else {
                    Logger
                            .log("haha, " + killer.getName()
                                    + " nuked himself!!");
                }
            } else { // default
                victim.setDeaths(victim.getDeaths() + 1);
                killer.setFrags(killer.getFrags() + 1);
                Logger.log(victim.getName() + " nuked by " + killer.getName());
            }

            /*
             * watch mode: only map border collision checks, and faster moving
             * for scrolling
             */
            if (this.me == victim) {
                this.me.setSpeed(Player.DEFAULT_SPEED * 4);
            }
        }
    }

    /**
     * send an event through the network. depending on the networkmanager
     * (client, server), the event will be delivered as unicast or broadcast.
     * 
     * @param e
     *            the event to send
     */
    protected void send(char id, String[] data) {
        try {
            this.networkManager.send(Event.encode(id, data));
        } catch (IOException e) {
            Logger.log("IOException in send");
            e.printStackTrace();
            throw new RuntimeException("send failed: " + e.getMessage());
        }
    }

}
